package com.ysjy.course.redis;

import com.alibaba.fastjson.JSON;
import com.wkbb.common.dto.common.RedisPropDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;


/**
 * 封装redis操作接口
 *
 * @author wuxq
 */
@Component
@Slf4j
public class RedisTemplateUtil {



    @Resource
    private RedisTemplate<String, Object> defaultTemplate;

    protected RedisTemplate<String, Object> redisTemplate;

    @PostConstruct
    public void init(){
        this.redisTemplate = defaultTemplate;
    }

    /**
     * 向链表尾端添加数据
     *
     * @param key
     * @param value
     */
    public void leftPush(String key, Object value) {
        redisTemplate.opsForList().leftPush(key, value);
    }

    /**
     * 从链表尾端弹出数据
     *
     * @param key
     */
    public Object leftPop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    /**
     * 向链表首端添加数据
     *
     * @param key
     * @param value
     */
    public void rightPush(String key, Object value) {
        redisTemplate.opsForList().rightPush(key, value);
    }

    /**
     * 从链表首端弹出数据
     *
     * @param key
     */
    public Object rightPop(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }

    /**
     * 获取链表长度
     *
     * @param key
     */
    public Long listCount(String key) {
        return redisTemplate.opsForList().size(key);
    }

    /**
     * 写入redis数据，注意会重置有效时间
     *
     * @param key
     * @param value
     */
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 写入redis数据，注意会重置有效时间
     *
     * @param key
     * @param value
     * @param timeout 过期时间
     *                时间单位：秒，小时等等。。默认为秒
     */
    public void set(String key, Object value, long timeout) {

        this.set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 写入redis数据，注意会重置有效时间
     *
     * @param key
     * @param value
     * @param timeout 过期时间
     * @param unit    时间单位：秒，小时等等。。默认为秒
     */
    public void set(String key, Object value, long timeout, TimeUnit unit) {
        // 有效期时间单位
        if (unit == null) {
            unit = TimeUnit.SECONDS;
        }
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
     * 生产环境禁用keys接口，先直接指向scankeys接口
     * @param key
     * @return
     */
    public Set<String> keys(String key) {
//        return redisTemplate.keys(key);
        return getScanKeys(key);
    }

    /**
     * 查询redis数据
     *
     * @param key
     * @return
     */
    public Object get(String key) {
        return get(key, true);
    }

    /**
     * 查询redis数据
     *
     * @param key
     * @return
     */
    public Object get(String key, boolean logFlag) {
        try {
            Object strVal = redisTemplate.opsForValue().get(key);
            if (logFlag && StringUtils.isEmpty(strVal)) {
                log.error("获取redis参数：{}的值为：{},请检查参数配置是否正确...", key, strVal);
            }
            //这里注意处理一下 缓存雪崩 和缓存击穿的问题
            return strVal;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 删除类似的Key信息 （数据多的时候会锁住redis库，慎用，使用scan代替）
     *
     * @param likeKey
     * @return
     */
    public void delKeysLike(final String likeKey) {
//        Set<String> keys = getLikeKeys(likeKey);
//        if (keys != null) {
//            keys.forEach(this::delete);
//        }
        delScanLike(likeKey);
    }

    /**
     * 获取类似的Key信息 （数据多的时候会锁住redis库，慎用，使用scan代替）
     * @param likeKey key
     * @return 相似key集合
     */
    public Set<String> getLikeKeys(String likeKey) {
//        return redisTemplate.keys(likeKey);
        return getScanKeys(likeKey);
    }


    /**
     * scan查询匹配的key
     * @param pattern	表达式
     * @param consumer	对迭代到的key进行操作
     */
    private void scan(String pattern, Consumer<byte[]> consumer) {
        this.redisTemplate.execute((RedisConnection connection) -> {
            try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(10000L).match(pattern).build())) {
                cursor.forEachRemaining(consumer);
                return null;
            } catch (IOException e) {
                log.error("获取scan查询异常，key：{}, msg：{}", pattern, e);
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * 获取符合条件的key
     * @param pattern	表达式
     * @return 符合条件的key集合
     */
    public Set<String> getScanKeys(String pattern) {
        Set<String> keys = new HashSet<>();
        this.scan(pattern, item -> {
            //符合条件的key
            keys.add(new String(item, StandardCharsets.UTF_8));
        });
        return keys;
    }

    /**
     * 删除符合条件的key
     * @param likeKey 符合条件的key
     */
    public void delScanLike(final String likeKey) {
        Set<String> keys = getScanKeys(likeKey);
        if (keys != null) {
            keys.forEach(this::delete);
        }
    }

    /**
     * 查询redis数据
     *
     * @param key
     * @return
     */
    public String getJSON(String key) {
        Object strVal = get(key);

        return JSON.toJSONString(strVal);
    }

    /**
     * Redis Hashes are maps between string fields and string values, so they are
     * the perfect data type to represent objects (e.g. A User with a number of
     * fields like name, surname, age, and so forth):
     *  @param key
     * @param map
     */
    public void hmset(String key, Map<String, String> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    /**
     * Get value for given {@code hashKey} from hash at {@code key}.
     *
     * @param key     must not be {@literal null}.
     * @param hashKey must not be {@literal null}.
     * @return
     */
    public Object hget(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    /**
     * Delete given hash {@code hashKeys}.
     */
    public void hdelete(String key, String hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }

    /**
     * 用 newValue 参数重写给定 key 所储存的字符串值，从偏移量 offset 开始,不会重置有效时间
     * <p>
     * 例如：template.opsForValue().set("key","hello world");
     * template.opsForValue().set("key","redis", 6);
     * System.out.println("***************"+template.opsForValue().get("key"));
     * 结果：***************hello redis
     *
     * @param key
     * @param newValue
     * @param offset
     */
    public void setJson(String key, Object newValue, long offset) {
        if (newValue != null) {
            String strVal = JSON.toJSONString(newValue);
            redisTemplate.opsForValue().set(key, strVal, offset);
            ;
        }
    }

    /**
     * 设置键的值并返回其旧值，注意会重置有效时间
     *
     * @param key
     * @return
     */
    public Object getAndSet(String key, Object newValue) {
        return redisTemplate.opsForValue().getAndSet(key, newValue);
    }


    /**
     * 返回key是否存在
     *
     * @param key
     * @return
     */
    public boolean hashKey(String key) {
        if (StringUtils.isEmpty(key)) {
            log.error("检查key:{}是否存在,key不能为null...", key);
            return false;
        }
        return redisTemplate.hasKey(key);
    }


    /**
     * 设置过期时间
     *
     * @param key
     * @param timeout
     * @param unit    默认为秒
     */
    public void expire(String key, long timeout, TimeUnit unit) {
        // 有效期时间单位
        if (unit == null) {
            unit = TimeUnit.SECONDS;
        }
        redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 根据key获取过期时间
     *
     * @param key
     * @param unit 非必填，默认为秒
     * @return
     */
    public Long getExpire(String key, TimeUnit unit) {
        if (unit == null) {
            return redisTemplate.getExpire(key);
        } else {
            return redisTemplate.getExpire(key, unit);
        }
    }


    /**
     * 返回连接
     *
     * @return
     */
    public RedisConnectionFactory getConnectionFactory() {
        return redisTemplate.getConnectionFactory();
    }

    /**
     * Redis 计数，不会重置有效期
     *
     * @param key
     * @param delta
     */
    public Double increment(String key, double delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * Redis 计数，不会重置有效期
     *
     * @param key
     * @param delta
     */
    public Long increment(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 设置自增计数器 并初始化过期时间
     *
     * @param key
     * @param delta   追加值
     * @param timeout 过期时间
     * @param unit    过期时间单位
     * @return
     */
    public long incrementForExpire(String key, long delta, long timeout, TimeUnit unit) {
        long count = redisTemplate.opsForValue().increment(key, delta);
        if (count == 1) {
            // 设置有效期
            if (unit == null) {
                unit = TimeUnit.SECONDS;
            }

            redisTemplate.expire(key, timeout, unit);
        }
        return count;
    }

    /**
     * 通过键查询列表；键为空查询所有数据
     *
     * @param key
     * @return
     */
    public List<RedisPropDto> queryListByKey(String key) {
        List<RedisPropDto> props = new ArrayList<>();

        ValueOperations<String, Object> values = redisTemplate.opsForValue();

        if (StringUtils.isEmpty(key)) {
            Set<String> keys = redisTemplate.keys("*");

            for (String k : keys) {
                Object val = values.get(k);
                RedisPropDto prop = new RedisPropDto();
                prop.setKey(k);
                prop.setValue(val);
                props.add(prop);
            }
        } else {
            Set<String> keys = redisTemplate.keys("*" + key + "*");

            for (String k : keys) {
                Object val = values.get(k);
                RedisPropDto prop = new RedisPropDto();
                prop.setKey(k);
                prop.setValue(val);
                props.add(prop);
            }
        }

        Collections.sort(props, new Comparator<RedisPropDto>() {
            @Override
            public int compare(RedisPropDto p1, RedisPropDto p2) {
                return p1.getKey().compareTo(p2.getKey());
            }
        });

        return props;
    }

    /**
     * 删除redis key
     *
     * @param key
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }
    /**
     * 批量删除
     * @param keys 符合条件的key SET
     */
    public void batchDel(final Set<String> keys) {
        keys.forEach(this::delete);
    }

    /**
     * 防重复提交接口。
     *
     * @param phone         用户标识,一般用手机号码
     * @param operationType 请使用OperationTypeConstant常量类里面定义的业务类型
     * @param timeout
     * @return true 重复提交，false 未重复提交
     */
    public boolean checkResubmit(String phone, String operationType, long timeout) {
        if (phone == null || operationType == null) {
            return true;
        }

        String key = new StringBuffer().append(operationType).append(":").append(phone).toString();
        boolean isResubmit = redisTemplate.hasKey(key);
        if (!isResubmit) {
            set(key, timeout, timeout, TimeUnit.MILLISECONDS);
        }

        return isResubmit;
    }

    /**
     * 防重复提交接口
     * 过期时间为1秒
     * 推荐使用
     *
     * @return boolean false:未重复提交，true:重复提交
     */
    public boolean checkResubmit_1000(String phone, String operationType) {
        return this.checkResubmit(phone, operationType, 1000);
    }

    /**
     * 防重复提交接口
     * 过期时间为1.5秒
     *
     * @return boolean false:未重复提交，true:重复提交
     */
    public boolean checkResubmit_1500(String phone, String operationType) {
        return this.checkResubmit(phone, operationType, 1500);
    }

    /**
     * 防重复提交接口
     * 过期时间为2秒
     * 不推荐使用
     *
     * @return boolean false:未重复提交，true:重复提交
     */
    public boolean checkResubmit_2000(String phone, String operationType) {
        return this.checkResubmit(phone, operationType, 2000);
    }

    /**
     * Sorted set :有序集合添加
     *
     * @param key
     * @param value
     * @param scoure
     */

    public void zAdd(String key, Object value, double scoure) {

        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();

        zset.add(key, value, scoure);

    }


    /**
     * Sorted set:有序集合获取
     *
     * @param key
     * @param scoure
     * @param scoure1
     * @return
     */

    public Set<Object> rangeByScore(String key, double scoure, double scoure1) {

        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();

        return zset.rangeByScore(key, scoure, scoure1);

    }

    /**
     * Sorted set:有序集合获取
     *
     * @param key
     * @param scoure
     * @param scoure1
     * @param offset  起始索引
     * @param count   返回个数
     * @return
     */

    public Set<Object> reverseRangeByScore(String key, double scoure, double scoure1, long offset, long count) {

        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();

        return zset.reverseRangeByScore(key, scoure, scoure1, offset, count);

    }
    /**
     *  set :无序集合添加
     *
     * @param key
     * @param  value
     * @return 被添加到集合中的新元素的数量
     */

    public Long sAdd(String key, Object... value) {

        return redisTemplate.opsForSet().add(key,value);
    }


    /**
     * set 返回无序集合中的所有的成员
     *
     * @param key
     * @return 集合中的所有的成员
     */

    public Set<Object> sMembers(String key) {

        return redisTemplate.opsForSet().members(key);
    }

    /**
     * set 返回无序集合中的所有的成员
     *
     * @param key
     * @param value 被移除的对象
     * @return  被成功移除的元素的数量，不包括被忽略的元素。
     */

    public Long sRemove(String key, Object... value) {

        return redisTemplate.opsForSet().remove(key,value);
    }

    /**
     * set 判断成员元素是否是集合的成员
     *
     * @param key
     * @param value
     * @return boolean
     */

    public Boolean isMember(String key,Object value) {
        return redisTemplate.opsForSet().isMember(key,value);
    }

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    public static final String UNLOCK_LUA;

    /**
     * 释放锁脚本，原子操作
     */
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }


    /**
     * 获取分布式锁，原子操作
     *
     * @param lockKey
     * @param requestId 唯一ID, 可以使用UUID.randomUUID().toString();
     * @param expire
     * @param timeUnit
     * @return
     */
    public boolean tryLock(String lockKey, String requestId, long expire, TimeUnit timeUnit) {
        try {
            RedisCallback<Boolean> callback = (connection) -> {
                return connection.set(lockKey.getBytes(Charset.forName("UTF-8")), requestId.getBytes(Charset.forName("UTF-8")), Expiration.milliseconds(timeUnit.toMillis(expire)), RedisStringCommands.SetOption.SET_IF_ABSENT);
            };
            return (Boolean) redisTemplate.execute(callback);
        } catch (Exception e) {
            log.error("redis lock error.", e);
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @param lockKey
     * @param requestId 唯一ID
     * @return
     */
    public boolean releaseLock(String lockKey, String requestId) {
        RedisCallback<Boolean> callback = (connection) -> {
            return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1, lockKey.getBytes(Charset.forName("UTF-8")), requestId.getBytes(Charset.forName("UTF-8")));
        };
        return (Boolean) redisTemplate.execute(callback);
    }

    /**
     * 获取Redis锁的value值
     *
     * @param lockKey
     * @return
     */
    public String getLockValue(String lockKey) {
        try {
            RedisCallback<String> callback = (connection) -> {
                return new String(connection.get(lockKey.getBytes()), Charset.forName("UTF-8"));
            };
            return (String) redisTemplate.execute(callback);
        } catch (Exception e) {
            log.error("get redis occurred an exception", e);
        }
        return null;
    }
}
